package org.yacon;

import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;

import static org.yacon.ConsoleEngineConstants.*;

/**
 * Facade of console engine.
 *
 * @author Evgeny Vetrov
 */
public class ConsoleEngine implements Serializable {
    private final ConsoleConfiguration configuration;
    private Program program;
    private Map<Class, Object> programContext;

    private static final Logger log = Logger.getLogger(ConsoleEngine.class.getName());


    private ConsoleEngine(ConsoleConfiguration configuration) {
        this.configuration = configuration;
    }


    /**
     * Creates ConsoleEngine based on on passed console configuration
     *
     * @param configuration console configuration
     * @return ConsoleEngine instance
     * @see ConsoleConfiguration#create(java.io.InputStream)
     * @see ConsoleConfiguration#create(String)
     */
    public static ConsoleEngine create(ConsoleConfiguration configuration) {
        return new ConsoleEngine(configuration);
    }

    /**
     * Executes User defined command
     *
     * @param command User defined command
     * @return result of command in String representation
     * @throws HelpException If validation error occurs.It may occur if command is not valid.
     */
    public String execute(String command) throws HelpException {
        CommandParser commandParser;
        try {
            commandParser = CommandParser.parse(command);
            if (!HELP.equals(commandParser.getCommandName()))
                CommandValidator.validate(commandParser, this.configuration, this.program);
        } catch (Exception e) {
            if (isPipe()) {
                return executePipe(command);
            } else {
                throw new HelpException(e.getMessage(), e);
            }
        }
        Map<String, Object> parametersValues = new HashMap<String, Object>(commandParser.getCommandParametersValues());
        if (HELP.equals(commandParser.getCommandName())) {
            return executeHelp(parametersValues);
        } else {
            ConsoleCommand conCmd;
            boolean destroyCommand = false;
            if (this.program != null) {
                conCmd = this.program.getCommandsMap().get(commandParser.getCommandName());
                if (conCmd == null)
                    return executePipe(command);
                if (conCmd.equals(this.program.getDestroyCommand()))
                    destroyCommand = true;
            } else
                conCmd = this.configuration.getCommand(commandParser.getCommandName());
            if (conCmd != null) {
                String result = execute(conCmd, parametersValues);
                if (destroyCommand) {
                    this.program = null;
                    programContext = null;
                }
                return result;
            } else {
                return executeProgram(commandParser, parametersValues);
            }
        }
    }

    private String executePipe(String command) {
        try {
            return this.program.getPipe().execute(command);
        } catch (RuntimeException e) {
            throw e;
        } catch (Exception e) {
            throw new RuntimeException(e.getMessage(), e);
        }
    }

    private boolean isPipe() {
        return this.program != null && this.program.getPipe() != null;
    }

    @SuppressWarnings("unchecked")
    private String executeProgram(CommandParser commandParser, Map<String, Object> parametersValues) {
        Program program = this.configuration.getProgram(commandParser.getCommandName());
        ConsoleCommand init = program.getInitCommand();
        String result = "Entered '" + commandParser.getCommandName() + "' program";
        this.program = program;
        this.programContext = new HashMap<Class, Object>();
        if (program.getPipe() != null)
            programContext.put(program.getPipe().getClass(), program.getPipe());
        if (init != null)
            try {
                result = execute(init, parametersValues);
                CommandHandler pipe = program.getPipe();
                if (pipe != null) {
                    if (pipe instanceof InitializableHandler) {
                        Object initInstance = programContext.get(init.getMethod().getDeclaringClass());
                        ((InitializableHandler) pipe).init(initInstance);
                    }
                }
            } catch (RuntimeException e) {
                this.program = null;
                this.programContext = null;
                throw e;
            } catch (Error e) {
                this.program = null;
                this.programContext = null;
                throw e;
            }
        if (program.getPipe() != null)
            programContext.put(program.getPipe().getClass(), program.getPipe());
        return result;
    }

    private String executeHelp(Map<String, Object> props) {
        String cmd = (String) props.get("value");
        cmd = cmd == null ? (String) props.get("cmd") : cmd;
        StringBuilder resultMessage = new StringBuilder();
        if (cmd == null) {
            resultMessage.append("Type \"help <command name>\" for full description of concrete command. " +
                    "Full list of commands are:\n");
            Collection<ConsoleCommand> cmds = this.program == null ? configuration.getCommands()
                    : this.program.getCommandsMap().values();
            cmds = new HashSet<ConsoleCommand>(cmds);
            for (ConsoleCommand command : cmds)
                appendNamesAndDescription(resultMessage, command.getNames(), command.getDescription());
            if (this.program == null) {
                Collection<Program> programs = configuration.getPrograms();
                for (Program program : programs)
                    appendNamesAndDescription(resultMessage, program.getNames(), program.getDescription());
            }
        } else {
            ConsoleCommand command = this.program == null ? configuration.getCommand(cmd)
                    : this.program.getCommandsMap().get(cmd);
            Program program = configuration.getProgram(cmd);
            if (command == null && program == null)
                resultMessage.append("There is no such command or program. " +
                        "Type \"help\" to see full list of commands and programs");
            else if (command != null)
                resultMessage.append(HelpMessageBuilder.fullCommandDescription(command));
            else
                resultMessage.append(HelpMessageBuilder.fullProgramDescription(program));
        }
        return resultMessage.toString();
    }

    private void appendNamesAndDescription(StringBuilder resultMessage, Set<String> names, String description) {
        resultMessage.append(names);
        if (description != null && description.length() > 0)
            resultMessage.append("\t--\t").append(description);
        resultMessage.append("\n");
    }

    private String execute(ConsoleCommand command, Map<String, Object> params) {
        TypeConverterHelper.applyConverter(params, command.getConverters());
        try {
            Method method = command.getMethod();
            Object instance = getInstance(method);
            return (String) method.invoke(instance, params);
        } catch (InvocationTargetException e) {
            String message = "Something went wrong. Please contact administrator. \n";
            log.log(Level.SEVERE, e.getMessage(), e);
            message += e.getCause().getMessage() == null ? "" : "Cause:  " + e.getCause().getMessage();
            throw new RuntimeException(message);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private Object getInstance(Method method) throws Exception {
        Class clazz = method.getDeclaringClass();
        Object instance;
        if (program != null && (instance = programContext.get(clazz)) != null)
            return instance;
        Constructor constructor = clazz.getDeclaredConstructor();
        constructor.setAccessible(true);
        instance = constructor.newInstance();
        if (program != null)
            programContext.put(clazz, instance);
        return instance;
    }

    @SuppressWarnings("UnusedDeclaration")
    static class ExitCommand {
        public String exit(Map<String, Object> args) {
            return "exited";
        }
    }
}